#include <ConnectionBroker/client/TcpClient.h>
#include <ConnectionBroker/Channels.h>
#include <ConnectionBroker/PacketUtils.h>
#include <ConnectionBroker/Packet.h>

#include <json/json.h>

#include <boost/bind.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/algorithm/string.hpp>

using namespace ConnectionBroker;

namespace
{
   const static std::string INVALID_CHANNEL_NAME = "Invalid Channel";
}

TcpClient::TcpClient(boost::asio::io_service& ioService, 
                     const ServerSettings& settings,
                     boost::shared_ptr<boost::asio::ssl::context> pContext)
: TcpConnection(true, ioService, pContext),
  mChannels(), 
  mOnMessageCallback(NULL), 
  mOnAuthenticationCallback(NULL), 
  mOnChannelsReceivedCallback(NULL), 
  mChannelsReceived(false), 
  mVersion("1.0.0"), 
  mConnected(false), 
  mSettings(settings)
{
}

TcpClient::~TcpClient()
{
}

void TcpClient::start()
{
   asyncConnect(mSettings.address(), mSettings.port());
}

void TcpClient::write(const Packet& pMessage)
{
   //Do not write if we are not connected.
   if(!mConnected)
   {
      mWritePackets.push_back(pMessage);
      return;
   }

   TcpConnection::send(pMessage);
}

void TcpClient::write(const std::string& channelName, const Json::Value& message)
{
   if(!mChannelsReceived)
   {
      mWriteChannelMessagePairs.push_back(std::make_pair(channelName, message));
      return;
   }

   if(!mChannels.count(channelName))
      return;

   Packet p(mChannels[channelName], message);

   write(p);
}

void TcpClient::setOnAuthenticationCallback(messageCallback pCallback)
{
   mOnAuthenticationCallback = pCallback;
}

void TcpClient::setOnMessageCallback(messageCallback pCallback)
{
   mOnMessageCallback = pCallback;
}

void TcpClient::setOnChannelsReceivedCallback(channelsCallback pCallback)
{
   mOnChannelsReceivedCallback = pCallback;
}

const std::string& TcpClient::channelName(const uint32_t channelNumber)
{
   for(std::map<std::string, uint32_t>::iterator it = mChannels.begin(); it != mChannels.end(); ++it)
   {
      if(channelNumber == it->second)
         return it->first;
   }

   return INVALID_CHANNEL_NAME;
}

void TcpClient::connectionEstablished()
{
   TcpConnection::connectionEstablished();

   mConnected = true;

   //Send the queued packets.
   while(!mWritePackets.empty())
   {
      Packet front = mWritePackets.front();
      mWritePackets.pop_front();
      write(front);
   }
}

void TcpClient::handleChannelListReceived(const Json::Value& pChannelList)
{
   fillChannelMap(pChannelList);
   mChannelsReceived = true;

   //Send the queue of channel name message pairs.
   while(!mWriteChannelMessagePairs.empty())
   {
      std::pair<std::string, Json::Value> front = mWriteChannelMessagePairs.front();
      mWriteChannelMessagePairs.pop_front(); 
      write(front.first, front.second);
   }

   if(!mOnChannelsReceivedCallback)
      return;

   std::vector<std::string> channelNames;

   for(std::map<std::string, uint32_t>::iterator it = mChannels.begin(); it != mChannels.end(); ++it)
      channelNames.push_back(it->first);

   mOnChannelsReceivedCallback(sharedFromThis(), channelNames);
}

void TcpClient::fillChannelMap(const Json::Value& pChannelList)
{
   mChannels.clear();
   Json::Value channelList(pChannelList.get("channels", Json::nullValue));
   if(channelList.isNull())
      return;

   for(int i = 0; i < channelList.size(); ++i)
   {   
      Json::Value channel(channelList[i]);
      std::vector<std::string> names(channel.getMemberNames());
      if(!names.size())
         continue;

      std::string name(names[0]);
      uint32_t value = channel.get(name, 0).asUInt();
      if(!value)
         continue;

      mChannels[name] = value;
   }
}

void TcpClient::messageReceived(const Packet& packet)
{

   uint32_t channel = packet.channel();
   Json::Value msgJson(packet.message());

   //Check the version.
   if(channel == Channels::LIBRARY_VERSION)
   {
      if(!versionCheck(msgJson))
      {
         onStop();
         return;
      }
   }

   //If this is the channel list, save the values into a map.
   if(channel == Channels::LIBRARY_CHANNEL_LIST)
   {
      handleChannelListReceived(msgJson);
      return;
   }

   //If this is on the authentication channel, we're authenticated and should invoke a callback.
   if(channel == Channels::LIBRARY_AUTHENTICATION)
   {
      if(!mOnAuthenticationCallback)
         return;

      mOnAuthenticationCallback(sharedFromThis(), packet);
      return;
   }

   //If we're in the library specific channels, return early.
   if(channel <= Channels::PREDEFINED_MAX)
      return;

   if(!mOnMessageCallback)
      return;

   mOnMessageCallback(sharedFromThis(), packet);
}

bool TcpClient::versionCheck(const Json::Value& msgJson)
{
   std::string serverVersion(msgJson.get("version", "").asString()); 
   std::vector<std::string> serverxyz;
   boost::split(serverxyz, serverVersion, boost::is_any_of("."));
   std::vector<std::string> clientxyz;
   boost::split(clientxyz, mVersion, boost::is_any_of("."));
   if(serverVersion.empty() || (serverxyz.size() != 3) || (serverxyz[0].compare(clientxyz[0])) || (serverxyz[1].compare(clientxyz[1])))
      return false;

   return true;
}

void TcpClient::subscribeTo(const std::string& channel)
{
   if(!mChannels.count(channel))
      return;

   uint32_t channelNumber = mChannels[channel];
   write(PacketUtils::channelNumberToPacket(channelNumber));
}

void TcpClient::sendAuthentication(const Json::Value& pAuthInfo)
{
   Packet p(Channels::LIBRARY_AUTHENTICATION, pAuthInfo);
   write(p);
}

void TcpClient::subscribeTo(const std::vector<std::string>& channels)
{
   std::vector<uint32_t> channelNumbers;

   for(std::vector<std::string>::const_iterator it = channels.begin(); it != channels.end(); ++it)
   {
      std::string channelName(*it);
      if(!mChannels.count(channelName))
         continue;

      channelNumbers.push_back(mChannels[channelName]);
   }

   if(!channelNumbers.size())
      return;

   write(PacketUtils::channelNumbersToPacket(channelNumbers));
}

void TcpClient::onStop()
{
   TcpConnection::onStop();
   mConnected = false;
}
